The same notebook, drawn with ECharts
A companion to the Plotly demo: the same three figures, the same three controls, the same data —
rendered through Slate's built-in echart DSL instead. Nothing is imported, because ECharts is core.
Each figure carries dataZoom=(type=:inside,), so the wheel zooms the chart rather than only the
page. That is deliberate: it is what exercises Slate's chart scroll-zoom gate, which holds the wheel
back until you click into a chart and then scales it by the Chart scroll-zoom setting.
@replay works here too, so the controls keep working in a standalone export with no kernel — the same
claim the Plotly notebook makes, reached through setOption instead of Plotly.restyle.
# No imports. `echart` and `series` are Slate builtins, so unlike the Plotly notebook — which opens
# `using PlotlyJS, SlatePlotly` — there is nothing to load and nothing to have installed.
"ECharts · built into Slate, no packages""ECharts · built into Slate, no packages"
# Monthly inflation: a mild cycle plus one sharp episode, narrow enough that the smoothing window
# visibly changes the peak. Identical to the Plotly notebook, so the two can be compared directly.
xs = collect(1:60)
infl = round.(2.0 .+ 0.45 .* sin.(xs ./ 3.2) .+ 0.25 .* cos.(xs ./ 1.7) .+
4.5 .* exp.(-((xs .- 31.0) .^ 2) ./ (2 * 1.1^2)); digits = 2)
# Centred moving average; the window shrinks at the ends rather than dropping points, so the smoothed
# series spans the whole range.
function movavg(v, w)
half = (w - 1) ÷ 2
[begin
lo, hi = max(1, i - half), min(length(v), i + half)
round(sum(view(v, lo:hi)) / (hi - lo + 1); digits = 2)
end for i in eachindex(v)]
end
"$(length(xs)) months · peak $(maximum(infl))%""60 months · peak 6.59%"
A @bind control driving two series
An ordinary Slate @bind — other cells can read w too.
Live, moving the slider re-runs the cell below in Julia: movavg(infl, w) is genuinely recomputed,
and ECharts animates the change in place rather than redrawing, which is why the smoothed line
slides rather than jumping.
In a standalone export there is no kernel, so @replay computes the series for every position the
control can take, packs them as one binary array, and ships it with the page. Moving the slider then
indexes into that data. The author never names a series or a field — the option is walked and the
routing worked out from where the marked value ended up.
Try the wheel over this chart before and after clicking into it: outside, the page scrolls; once the chart has focus its border lights up and the wheel zooms the x range.
echart(series(:line, xs, infl; name = "monthly", lineStyle = (width = 1, opacity = 0.45), symbol = "none"),
series(:line, xs, @replay(w, movavg(infl, w)); name = "smoothed", lineStyle = (width = 3,), symbol = "none");
title = "Centred moving average",
height = 420,
legend = true,
xAxis = (name = "month",),
yAxis = (name = "inflation (%)", min = 0, max = 7),
dataZoom = [(type = :inside,)])A matrix, and a different kind of control
@replay is not limited to a series or to a slider. Here a whole matrix is replayed, driven by a
Select. The slices stack along a new trailing dimension, so one control value is still a contiguous
run in the shipped buffer — the page takes a view and reshapes it, never a gather.
Both axes take an inside dataZoom, so the wheel zooms the field in x and y together.
field(k) = [k == "gaussian" ? exp(-((x - 40)^2 + (y - 30)^2) / 320) :
k == "ripple" ? sin(hypot(x - 40, y - 30) / 4) :
k == "saddle" ? ((x - 40)^2 - (y - 30)^2) / 1600 :
-exp(-((x - 40)^2 + (y - 30)^2) / 900)
for y in 1:60, x in 1:80]
echart(:heatmap, @replay(kern, field(kern));
title = "60 × 80 field",
height = 420,
dataZoom = [(type = :inside, xAxisIndex = 0), (type = :inside, yAxisIndex = 0)])Scale
A 2 000-point series over a 100-position slider: 200 000 values, 1.6 MB packed. The cost is the
whole shipped array — points × domain size × 8 bytes for Float64 — and the control still moves at
full speed, because switching position is a contiguous read, not a recomputation.
This is also the chart to zoom hard on: the x axis relabels repeatedly as the range shrinks, which is exactly the redraw path that made the Plotly figures judder before the margin handling was fixed.
using Random
big_x = collect(1:2000)
big_y = cumsum(randn(Random.Xoshiro(42), 2000)) .+ 40
smooth(v, σ) = [sum(@view v[max(1, i - σ):min(length(v), i + σ)]) /
(min(length(v), i + σ) - max(1, i - σ) + 1) for i in eachindex(v)]
echart(series(:line, big_x, big_y; name = "raw", lineStyle = (width = 0.7, opacity = 0.4), symbol = "none"),
series(:line, big_x, @replay(σ, smooth(big_y, σ)); name = "smoothed", lineStyle = (width = 2.5,), symbol = "none");
title = "2 000 points × 100 slider positions",
height = 400,
legend = true,
xAxis = (name = "t",),
yAxis = (name = "level",),
dataZoom = [(type = :inside,)])What survives the export
Export with Offline ticked for a single file with no external subresources. Because the notebook
has @replay marks, a second dialog appears first: each one lists what it will compute and how many
bytes it will carry — measured from a real value, not estimated — and lets you lower a slider's
resolution to trade fidelity for size.
Nothing ECharts-specific was written to make that work. The mark, the packed sweep and the export
table are Slate's; what the renderer contributes is a walk of its own option to find where a marked
value ended up, and one call to put a slice back. For ECharts that walk records a PATH — series 1 data, or the heatmap's data — and the page rebuilds the smallest option addressing it, so
setOption merges just that field and the reader's zoom, roam and legend state survive every step of
a drag.